# Movable Type (r) Open Source (C) 2001-2010 Six Apart, Ltd.
# This program is distributed under the terms of the
# GNU General Public License, version 2.
#
# $Id: Asset.pm 5417 2010-05-12 02:58:17Z takayama $
package MT::CMS::Asset;

use strict;
use Symbol;
use MT::Util qw( epoch2ts encode_url format_ts relative_date );

sub edit {
    my $cb = shift;
    my ($app, $id, $obj, $param) = @_;

    if ($id) {
        my $asset_class = $app->model('asset');
        $param->{asset} = $obj;
        $param->{search_label} = $app->translate('Assets');

        my $hasher = build_asset_hasher($app);
        $hasher->($obj, $param, ThumbWidth => 240, ThumbHeight => 240);

        my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
        require MT::Tag;
        my $tags = MT::Tag->join( $tag_delim, $obj->tags );
        $param->{tags} = $tags;

        if ($tag_delim) {
            if ( $tag_delim eq ',' ) {
                $param->{'auth_pref_tag_delim_comma'} = 1;
            }
            elsif ( $tag_delim eq ' ' ) {
                $param->{'auth_pref_tag_delim_space'} = 1;
            }
            else {
                $param->{'auth_pref_tag_delim_other'} = 1;
            }
            $param->{'auth_pref_tag_delim'} = $tag_delim;
        }
        require MT::ObjectTag;
        $param->{tags_js} = MT::Util::to_json(
            [   map { $_->name } MT::Tag->load(
                    undef,
                    {   join => [
                            'MT::ObjectTag', 'tag_id',
                            { blog_id => $obj->blog_id }, { unique => 1 }
                        ]
                    }
                )
            ]
        );

        my @related;
        if ($obj->parent) {
            my $parent = $asset_class->load($obj->parent);
            push @related, $hasher->($parent, { asset => $parent, is_parent => 1 });

            push @related, map { $hasher->($_, { asset => $_, is_sibling => 1 }) }
                $asset_class->search({
                    id     => { op => '!=', value => $obj->id },
                    class  => '*',
                    parent => $obj->parent
                });
        }
        push @related, map { $hasher->($_, { asset => $_, is_child => 1 }) }
            $asset_class->search({
                class  => '*',
                parent => $obj->id,
            });
        $param->{related} = \@related if @related;

        my @appears_in;
        my $place_class = $app->model('objectasset');
        my $place_iter = $place_class->load_iter(
            {
                blog_id => $obj->blog_id || 0,
                asset_id => $obj->parent ? $obj->parent : $obj->id
            }
        );
        while (my $place = $place_iter->()) {
            my $entry_class = $app->model($place->object_ds) or next;
            next unless $entry_class->isa('MT::Entry');
            my $entry = $entry_class->load($place->object_id)
                or next;
            my %entry_data = (
                id    => $place->object_id,
                class => $entry->class_type,
                entry => $entry,
                title => $entry->title,
            );
            if (my $ts = $entry->authored_on) {
                $entry_data{authored_on_ts} = $ts;
                $entry_data{authored_on_formatted} =
                  format_ts( MT::App::CMS::LISTING_DATETIME_FORMAT(), $ts, undef,
                    $app->user ? $app->user->preferred_language : undef );
            }
            if (my $ts = $entry->created_on) {
                $entry_data{created_on_ts} = $ts;
                $entry_data{created_on_formatted} =
                  format_ts( MT::App::CMS::LISTING_DATETIME_FORMAT(), $ts, undef,
                    $app->user ? $app->user->preferred_language : undef );
            }
            push @appears_in, \%entry_data;
        }
        if (11 == @appears_in) {    
            pop @appears_in;
            $param->{appears_in_more} = 1;
        }
        $param->{appears_in} = \@appears_in if @appears_in;

        my $prev_asset = $obj->nextprev(
            direction => 'previous',
            terms     => { class => '*', blog_id => $obj->blog_id },
        );
        my $next_asset = $obj->nextprev(
            direction => 'next',
            terms     => { class => '*', blog_id => $obj->blog_id },
        );
        $param->{previous_entry_id} = $prev_asset->id if $prev_asset;
        $param->{next_entry_id}     = $next_asset->id if $next_asset;

        my $user = MT::Author->load(
            {
                id   => $obj->created_by(),
                type => MT::Author::AUTHOR()
            }
        );
        if ($user) {
            $param->{created_by} = $user->name;
        } else {
            $param->{created_by} = $app->translate('(user deleted)');
        }
        if ( $obj->modified_by() ) {
            $user = MT::Author->load(
                {
                    id   => $obj->modified_by(),
                    type => MT::Author::AUTHOR()
                }
            );
            if ($user) {
                $param->{modified_by} = $user->name;
            } else {
                $param->{modified_by} = $app->translate('(user deleted)');
            }
        }
    }
    1;
}

sub list {
    my $app = shift;

    my $blog_id = $app->param('blog_id');
    my $mode_userpic = $app->param('upload_mode') || '';
    return $app->return_to_dashboard( redirect => 1 )
        if  !$blog_id && $mode_userpic ne 'upload_userpic';

    my $blog_class = $app->model('blog');
    my $blog = $blog_class->load($blog_id) if $blog_id;
    my $perms = $app->permissions;
    return $app->errtrans("Permission denied.")
        if $mode_userpic ne 'upload_userpic' && !$app->can_do('access_to_asset_list');

    my $asset_class = $app->model('asset') or return;
    my %terms;
    my %args = ( sort => 'created_on', direction => 'descend' );

    my $class_filter;
    my $filter = ( $app->param('filter') || '' );
    if ( $filter eq 'class' ) {
        $class_filter = $app->param('filter_val');
    }
    elsif ($filter eq 'userpic') {
        $class_filter = 'image';
        $terms{created_by} = $app->param('filter_val');

        my $tag = MT::Tag->load( { name => '@userpic' },
            { binary => { name => 1 } } );
        if ($tag) {
            require MT::ObjectTag;
            $args{'join'} = MT::ObjectTag->join_on(
                'object_id',
                {
                    tag_id            => $tag->id,
                    object_datasource => MT::Asset->datasource
                },
                { unique => 1 }
            );
        }
    }

    $app->add_breadcrumb( $app->translate("Files") );


    if ($blog_id) {
        my $blog_ids = $app->_load_child_blog_ids($blog_id);
        push @$blog_ids, $blog_id;
        $terms{blog_id} = $blog_ids;
    }

    my $hasher = build_asset_hasher( $app,
        PreviewWidth => 240, PreviewHeight => 240 );

    if ($class_filter) {
        my $asset_pkg = MT::Asset->class_handler($class_filter);
        $terms{class} = $asset_pkg->type_list;
    }
    else {
        $terms{class} = '*';    # all classes
    }

    # identifier => name
    my $classes = MT::Asset->class_labels;
    my @class_loop;
    foreach my $class ( keys %$classes ) {
        next if $class eq 'asset';
        push @class_loop,
          {
            class_id    => $class,
            class_label => $classes->{$class},
          };
    }

    # Now, sort it
    @class_loop = sort { $a->{class_label} cmp $b->{class_label} } @class_loop;

    my $dialog_view = $app->param('dialog_view') ? 1 : 0;
    my $dialog = $app->param('dialog') ? 1 : 0;
    my $no_insert = $app->param('no_insert') ? 1 : 0;
    my %carry_params = map { $_ => $app->param($_) || '' }
        (qw( edit_field upload_mode require_type next_mode asset_select ));
    $carry_params{'user_id'} = $app->param('filter_val')
        if $filter eq 'userpic';
    _set_start_upload_params($app, \%carry_params);
    my ($ext_from, $ext_to) = ( $app->param('ext_from'), $app->param('ext_to') );
    $app->listing(
        {
            terms    => \%terms,
            args     => \%args,
            type     => 'asset',
            code     => $hasher,
            template => $dialog_view
            ? 'dialog/asset_list.tmpl'
            : '',
            ( $app->param('search') ? ( no_limit => 1 ) : () ),
            params => {
                (
                    $blog
                    ? (
                        blog_id   => $blog_id,
                        blog_name => $blog->name
                          || '',
                        edit_blog_id => $blog_id,
                        ($blog->is_blog ? (blog_view => 1) : ()),
                      )
                    : (),
                ),
                is_image         => defined $class_filter
                  && $class_filter eq 'image' ? 1 : 0,
                dialog_view      => $dialog_view,
                dialog           => $dialog,
                no_insert        => $no_insert,
                search_label     => MT::Asset->class_label_plural,
                search_type      => 'asset',
                class_loop       => \@class_loop,
                can_delete_files => $app->can_do('delete_asset_file'),
                nav_assets       => 1,
                panel_searchable => 1,
                saved_deleted    => $app->param('saved_deleted') ? 1 : 0,
                object_type      => 'asset',
                (
                    ( $ext_from && $ext_to )
                        ? ( ext_from => $ext_from, ext_to => $ext_to )
                        : ()
                ),
                %carry_params,
            },
        }
    );
}

sub insert {
    my $app  = shift;
    my $text = $app->param('no_insert') ? "" : _process_post_upload( $app );
    return unless defined $text;
    my $file_ext_changes = $app->param('changed_file_ext');
    my ($ext_from, $ext_to) = split(",", $file_ext_changes) if $file_ext_changes;
    my $extension_message = $app->translate("Extension changed from [_1] to [_2]", $ext_from, $ext_to) if ($ext_from && $ext_to);
    my $tmpl;
    if ($extension_message) {
        $tmpl = $app->load_tmpl(
            'dialog/asset_insert.tmpl',
            {
                upload_html => $text || '',
                edit_field => scalar $app->param('edit_field') || '',
                extension_message => $extension_message,
            },
        );
    } else {
       $tmpl = $app->load_tmpl(
            'dialog/asset_insert.tmpl',
            {
                upload_html => $text || '',
                edit_field => scalar $app->param('edit_field') || '',
            },
        );
    }
    my $ctx = $tmpl->context;
    my $id = $app->param('id') or return $app->errtrans("Invalid request.");
    my $asset = MT::Asset->load( $id );
    $ctx->stash('asset', $asset);
    return $tmpl;
}

sub asset_userpic {
    my $app = shift;
    my ($param) = @_;

    my ($id, $asset);
    if ($asset = $param->{asset}) {
        $id = $asset->id;
    }
    else {
        $id = $param->{asset_id} || scalar $app->param('id');
        $asset = $app->model('asset')->lookup($id);
    }

    my $user_id = $param->{user_id} || $app->param('user_id');
    my $user;
    if ($user_id) {
        $user = $app->model('author')->load( {id => $user_id} );
        if ($user) {
            # Delete the author's userpic thumb (if any); it'll be regenerated.
            if ($user->userpic_asset_id != $asset->id) {
                my $old_file = $user->userpic_file();
                my $fmgr = MT::FileMgr->new('Local');
                if ($fmgr->exists($old_file)) {
                    $fmgr->delete($old_file);
                }
                $user->userpic_asset_id($asset->id);
                $user->save;
            }
        }
    }

    my $thumb_html = $user
        ? $user->userpic_html( Asset => $asset )
        : $app->model('author')->userpic_html( Asset => $asset );

    $app->load_tmpl(
        'dialog/asset_userpic.tmpl',
        {
            asset_id       => $id,
            edit_field     => $app->param('edit_field') || '',
            author_userpic => $thumb_html,
        },
    );
}

sub start_upload {
    my $app = shift;
    my $dialog = $app->param('dialog');
    $dialog =~ s/\D//g;
    return $app->return_to_dashboard( redirect => 1 )
        if !$app->blog && !$dialog;

    $app->add_breadcrumb( $app->translate('Upload File') );
    my %param;
    %param = @_ if @_;

    _set_start_upload_params($app, \%param);

    for my $field (qw( entry_insert edit_field upload_mode require_type
      asset_select )) {
        $param{$field} ||= $app->param($field);
    }

    if ( $app->param('uploaded') ) {
        $param{uploaded} = 1;
        $param{uploaded_filename} = $app->param('uploaded_filename');
    }

    $param{search_label} = $app->translate('Assets');
    $param{search_type}  = 'asset';

    $param{dialog} = $dialog;
    my $tmpl_file = $dialog ? 'dialog/asset_upload.tmpl' : 'asset_upload.tmpl';
    $app->load_tmpl( $tmpl_file, \%param );
}

sub upload_file {
    my $app = shift;

    my ($asset, $bytes) = _upload_file( $app,
        require_type => ($app->param('require_type') || ''),
        @_,
    );
    return if !defined $asset;
    return $asset if !defined $bytes;  # whatever it is
    complete_insert( $app,
        asset => $asset,
        bytes => $bytes,
    );
}

sub complete_insert {
    my $app = shift;
    my (%args) = @_;
    my $asset = $args{asset};
    if ( !$asset && $app->param('id') ) {
        require MT::Asset;
        $asset = MT::Asset->load( $app->param('id') )
          || return $app->errtrans( "Can't load file #[_1].",
            $app->param('id') );
    }
    return $app->errtrans('Invalid request.') unless $asset;

    $args{is_image} = $asset->isa('MT::Asset::Image') ? 1 : 0 unless defined $args{is_image};

    require MT::Blog;
    my $blog = $asset->blog
      or
      return $app->errtrans( "Can't load blog #[_1].", $app->param('blog_id') );
    my $perms = $app->permissions
      or return $app->errtrans('No permissions');

    # caller wants asset without any option step, so insert immediately
    if ($app->param('asset_select') || $app->param('no_insert')) {
        $app->param( 'id', $asset->id );
        return insert($app);
    }
    
    my $param = {
        asset_id            => $asset->id,
        bytes               => $args{bytes},
        fname               => $asset->file_name,
        is_image            => $args{is_image} || 0,
        url                 => $asset->url,
        middle_path         => $app->param('middle_path') || '',
        extra_path          => $app->param('extra_path') || '',
    };
    for my $field (qw( direct_asset_insert edit_field entry_insert site_path
      asset_select )) {
        $param->{$field} = scalar $app->param($field) || '';
    }
    if ( $args{is_image} ) {
        $param->{width}  = $asset->image_width;
        $param->{height} = $asset->image_height;
    }
    my ($extension_message, $ext_from, $ext_to);
    if ($app->param('changed_file_ext')) {
        ($ext_from, $ext_to) = split(",", $app->param('changed_file_ext'));
        $extension_message = $app->translate("Extension changed from [_1] to [_2]", $ext_from, $ext_to) if ($ext_from && $ext_to);
        $param->{extension_message} = $extension_message;
        $param->{ext_from} = $ext_from;
        $param->{ext_to} = $ext_to;
    }
    if ( !$app->param('asset_select')
      && ($perms->can_do('insert_asset')) ) {
        my $html = $asset->insert_options($param);
        if ( $param->{direct_asset_insert} && !$html ) {
            $app->param( 'id', $asset->id );
            return insert($app);
        }
        $param->{options_snippet} = $html;
    }
    if ($perms) {
        my $pref_param = $app->load_entry_prefs(
            { type => 'entry', prefs => $perms->entry_prefs } );
        %$param = ( %$param, %$pref_param );

        # Completion for tags
        my $author     = $app->user;
        my $auth_prefs = $author->entry_prefs;
        if ( my $delim = chr( $auth_prefs->{tag_delim} ) ) {
            if ( $delim eq ',' ) {
                $param->{'auth_pref_tag_delim_comma'} = 1;
            }
            elsif ( $delim eq ' ' ) {
                $param->{'auth_pref_tag_delim_space'} = 1;
            }
            else {
                $param->{'auth_pref_tag_delim_other'} = 1;
            }
            $param->{'auth_pref_tag_delim'} = $delim;
        }

        require MT::ObjectTag;
        my $q       = $app->param;
        my $blog_id = $q->param('blog_id');
        $param->{tags_js} = MT::Util::to_json(
            [   map { $_->name } MT::Tag->load(
                    undef,
                    {   join => [
                            'MT::ObjectTag', 'tag_id',
                            { blog_id => $blog_id }, { unique => 1 }
                        ]
                    }
                )
            ]
        );
    }

    $param->{'no_insert'} = $app->param('no_insert');
    if ( $app->param('dialog') ) {
        $app->load_tmpl( 'dialog/asset_options.tmpl', $param );
    }
    else {
        if ( $app->user->can_do('access_to_asset_list') ) {
            my $redirect_args = {
                blog_id => $app->param('blog_id'),
                ( ( $ext_from && $ext_to )
                    ? ( ext_from => $ext_from, ext_to => $ext_to )
                    : ()
                ),
            };
            $app->redirect(
                $app->uri(
                    'mode' => 'list_asset',
                    args   => $redirect_args,
                )
            );
        } else {
            $app->redirect(
                $app->uri(
                    'mode' => 'start_upload',
                    args   => {
                        blog_id => $app->param('blog_id'),
                        uploaded => 1,
                        uploaded_filename => $asset->file_name,
                    },
                )
            );
        }
    }
}

sub complete_upload {
    my $app   = shift;
    my %param = $app->param_hash;
    my $asset;
    require MT::Asset;
    $param{id} && ( $asset = MT::Asset->load( $param{id} ) )
      or return $app->errtrans("Invalid request.");
    $asset->label( $param{label} )             if $param{label};
    $asset->description( $param{description} ) if $param{description};
    if ( $param{tags} ) {
        require MT::Tag;
        my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
        my @tags = MT::Tag->split( $tag_delim, $param{tags} );
        $asset->set_tags(@tags);
    }
    $asset->save();
    $asset->on_upload( \%param );

    my $perms = $app->permissions;
    return $app->return_to_dashboard( permission => 1 )
        unless $app->can_do('access_to_asset_list');

    return $app->redirect(
        $app->uri(
            'mode' => 'list_asset',
            args   => { 'blog_id' => $app->param('blog_id') }
        )
    );
}

sub start_upload_entry {
    my $app = shift;
    my $q   = $app->param;
    my $blog = $app->blog;
    my $type = 'entry';
    $type = 'page'
        if ($blog && !$blog->is_blog());
    $q->param( '_type', $type );
    defined( my $text = _process_post_upload($app) ) or return;
    $q->param( 'text', $text );
    $q->param ('asset_id', $q->param('id'));
    $q->param( 'id', 0 );
    $app->param( 'tags', '' );
    $app->forward("view");
}

sub can_view {
    my ($eh, $app, $id) = @_;
    return $app->can_do('open_asset_edit_screen');
}

sub can_delete {
    my ( $eh, $app, $obj ) = @_;
    return $app->can_do('delete_asset');
}

sub pre_save {
    my $eh = shift;
    my ( $app, $obj ) = @_;

    # save tags
    my $tags = $app->param('tags');
    if ( defined $tags ) {
        my $blog = $app->blog;
        my $fields = $blog ? $blog->smart_replace_fields
            : MT->config->NwcReplaceField;
        if ( $fields && $fields =~ m/tags/ig ) {
            $tags = MT::App::CMS::_convert_word_chars( $app, $tags );
        }

        require MT::Tag;
        my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
        my @tags = MT::Tag->split( $tag_delim, $tags );
        if (@tags) {
            $obj->set_tags(@tags);
        }
        else {
            $obj->remove_tags();
        }
    }
    1;
}

sub post_save {
    my $eh = shift;
    my ( $app, $obj, $original ) = @_;

    if ( !$original->id ) {
        $app->log(
            {
                message => $app->translate(
                    "File '[_1]' uploaded by '[_2]'", $obj->file_name,
                    $app->user->name
                ),
                level    => MT::Log::INFO(),
                class    => 'asset',
                category => 'new',
            }
        );
    }
    1;
}

sub post_delete {
    my ( $eh, $app, $obj ) = @_;

    $app->log(
        {
            message => $app->translate(
                "File '[_1]' (ID:[_2]) deleted by '[_3]'",
                $obj->file_name, $obj->id, $app->user->name
            ),
            level    => MT::Log::INFO(),
            class    => 'asset',
            category => 'delete'
        }
    );
}

sub template_param_edit {
    my ($cb, $app, $param, $tmpl) = @_;
    my $asset = $param->{asset} or return;
    $asset->edit_template_param(@_);
}

sub asset_list_filters {
    my $app = shift;

    my %filters;
    my $types = MT::Asset->class_labels;
    foreach my $type ( keys %$types ) {
        my $asset_type = $type;
        $asset_type =~ s/^asset\.//;
        $filters{$asset_type} = {
            label   => sub { MT::Asset->class_handler($type)->class_label_plural },
            handler => sub {
                my ( $terms, $args ) = @_;
                $terms->{class} = $asset_type eq 'asset' ? '*' : $asset_type;
            },
        };
    }

    # If current context is website, adding the website filter.
    if ( $app->blog && !$app->blog->is_blog ) {
        $filters{'this_website'} = {
            label => $app->translate('[_1] of this website', MT::Asset->class_label_plural),
            order => 100,
            handler => sub {
                my ( $terms, $args ) = @_;
                $terms->{blog_id} = $app->blog->id;
            },
        };
    }

    my @types =
      sort { $filters{$a}{label} cmp $filters{$b}{label} } keys %filters;
    my $order = 200;
    foreach (@types) {
        $filters{$_}{order} = $order;
        $order += 100;
    }
    $filters{'asset'}{order} = 0;
    $filters{'asset'}{label} = "All Assets"; # labels are translated later
                                             # translate("All Assets");
    return \%filters;
}

sub build_asset_hasher {
    my $app = shift;
    my (%param) = @_;
    my ($default_thumb_width, $default_thumb_height, $default_preview_width,
        $default_preview_height) =
        @param{qw( ThumbWidth ThumbHeight PreviewWidth PreviewHeight )};

    require File::Basename;
    require JSON;
    my %blogs;
    return sub {
        my ( $obj, $row, %param ) = @_;
        my ($thumb_width, $thumb_height) = @param{qw( ThumbWidth ThumbHeight )};
        $row->{id} = $obj->id;
        my $blog = $blogs{ $obj->blog_id } ||= $obj->blog;
        $row->{blog_name} = $blog ? $blog->name : '-';
        $row->{url} = $obj->url; # this has to be called to calculate
        $row->{asset_type} = $obj->class_type;
        $row->{asset_class_label} = $obj->class_label;
        my $file_path = $obj->file_path; # has to be called to calculate
        my $meta = $obj->metadata;

        require MT::FileMgr;
        my $fmgr = MT::FileMgr->new('Local');
        ## TBD: Make sure $file_path is file, not directory.
        if ( $file_path && $fmgr->exists( $file_path ) ) {
            $row->{file_path} = $file_path;
            $row->{file_name} = File::Basename::basename( $file_path );
            my $size = $fmgr->file_size( $file_path );
            $row->{file_size} = $size;
            if ( $size < 1024 ) {
                $row->{file_size_formatted} = sprintf( "%d Bytes", $size );
            }
            elsif ( $size < 1024000 ) {
                $row->{file_size_formatted} =
                  sprintf( "%.1f KB", $size / 1024 );
            }
            else {
                $row->{file_size_formatted} =
                  sprintf( "%.1f MB", $size / 1024000 );
            }
            $meta->{'file_size'} = $row->{file_size_formatted};
        }
        else {
            $row->{file_is_missing} = 1 if $file_path;
        }
        $row->{file_label} = $row->{label} = $obj->label || $row->{file_name} || $app->translate('Untitled');

        if ($obj->has_thumbnail) { 
            $row->{has_thumbnail} = 1;
            my $height = $thumb_height || $default_thumb_height || 75;
            my $width  = $thumb_width  || $default_thumb_width  || 75;
            my $square = $height == 75 && $width == 75;
            @$meta{qw( thumbnail_url thumbnail_width thumbnail_height )}
              = $obj->thumbnail_url( Height => $height, Width => $width , Square => $square );

            $meta->{thumbnail_width_offset}  = int(($width  - $meta->{thumbnail_width})  / 2);
            $meta->{thumbnail_height_offset} = int(($height - $meta->{thumbnail_height}) / 2);

            if ($default_preview_width && $default_preview_height) {
                @$meta{qw( preview_url preview_width preview_height )}
                  = $obj->thumbnail_url(
                    Height => $default_preview_height,
                    Width  => $default_preview_width,
                );
                $meta->{preview_width_offset}  = int(($default_preview_width  - $meta->{preview_width})  / 2);
                $meta->{preview_height_offset} = int(($default_preview_height - $meta->{preview_height}) / 2);
            }
        }
        else {
            $row->{has_thumbnail} = 0;
        }

        my $ts = $obj->created_on;
        if ( my $by = $obj->created_by ) {
            my $user = MT::Author->load($by);
            $row->{created_by} = $user ? $user->name : $app->translate('(user deleted)');
        }
        if ($ts) {
            $row->{created_on_formatted} =
              format_ts( MT::App::CMS::LISTING_DATE_FORMAT(), $ts, $blog, $app->user ? $app->user->preferred_language : undef );
            $row->{created_on_time_formatted} =
              format_ts( MT::App::CMS::LISTING_TIMESTAMP_FORMAT(), $ts, $blog, $app->user ? $app->user->preferred_language : undef );
            $row->{created_on_relative} = relative_date( $ts, time, $blog );
        }

        @$row{keys %$meta} = values %$meta;
        $row->{metadata_json} = MT::Util::to_json($meta);
        $row;
    };
}

sub build_asset_table {
    my $app = shift;
    my (%args) = @_;

    my $asset_class = $app->model('asset') or return;
    my $perms     = $app->permissions;
    my $list_pref = $app->list_pref('asset');
    my $limit     = $args{limit};
    my $param     = $args{param} || {};
    my $iter;
    if ( $args{load_args} ) {
        my $class = $app->model('asset');
        $iter = $class->load_iter( @{ $args{load_args} } );
    }
    elsif ( $args{iter} ) {
        $iter = $args{iter};
    }
    elsif ( $args{items} ) {
        $iter = sub { pop @{ $args{items} } };
        $limit = scalar @{ $args{items} };
    }
    return [] unless $iter;

    my @data;
    my $hasher = build_asset_hasher($app);
    while ( my $obj = $iter->() ) {
        my $row = $obj->get_values;
        $hasher->($obj, $row);
        $row->{object} = $obj;
        push @data, $row;
        last if $limit and @data > $limit;
    }
    return [] unless @data;

    $param->{template_table}[0]              = {%$list_pref};
    $param->{template_table}[0]{object_loop} = \@data;
    $param->{template_table}[0]{object_type} = 'asset';
    $app->load_list_actions( 'asset', $param );
    $param->{object_loop} = \@data;
    $param->{can_delete_files} = 1
        if $app->can_do('delete_asset_file');
    \@data;
}

sub asset_insert_text {
    my $app     = shift;
    my ($param) = @_;
    my $q       = $app->param;
    my $id      = $app->param('id')
      or return $app->errtrans("Invalid request.");
    require MT::Asset;
    my $asset = MT::Asset->load($id)
      or return $app->errtrans( "Can't load file #[_1].", $id );
    $param->{enclose} = $app->param('edit_field') =~ /^customfield/ ? 1 : 0;
    return $asset->as_html($param);
}

sub _process_post_upload {
    my $app   = shift;
    my %param = $app->param_hash;
    my $asset;
    require MT::Asset;
    $param{id} && ( $asset = MT::Asset->load( $param{id} ) )
      or return $app->errtrans("Invalid request.");
    $asset->label( $param{label} )             if $param{label};
    $asset->description( $param{description} ) if $param{description};
    if ( $param{tags} ) {
        require MT::Tag;
        my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
        my @tags = MT::Tag->split( $tag_delim, $param{tags} );
        $asset->set_tags(@tags);
    }
    $asset->save();

    $asset->on_upload( \%param );
    return asset_insert_text( $app, \%param );
}

# FIXME: need to make this work
sub save {
    my $app   = shift;
    my $q     = $app->param;
    my $perms = $app->permissions;
    my $type  = $q->param('_type');
    my $class = $app->model($type)
      or return $app->errtrans("Invalid request.");

    $app->validate_magic() or return;
    return $app->errtrans("Permission denied.")
        unless $app->can_do('save_asset');

    my $blog_id = $q->param('blog_id');
    my $id = $q->param('id');
    my $obj = $id ? $class->load($id) : $class->new;
    return unless $obj;
    my $original = $obj->clone();

    $obj->set_values_from_query($q);

    my $filter_result = $app->run_callbacks( 'cms_save_filter.' . $type, $app );
    if ( !$filter_result ) {
        my %param = ();
        $param{error}       = $app->errstr;
        $param{return_args} = $app->param('return_args');
        return $app->forward( "view", \%param );
    }

    $app->run_callbacks( 'cms_pre_save.' . $type, $app, $obj, $original )
      || return $app->errtrans( "Saving [_1] failed: [_2]", $type,
        $app->errstr );

    $obj->save
      or return $app->error(
        $app->translate(
            "Saving [_1] failed: [_2]",
            $type, $obj->errstr
        )
      );

    $app->run_callbacks( 'cms_post_save.' . $type, $app, $obj, $original );

    $app->redirect(
        $app->uri(
            'mode' => 'view',
            args   => {
                _type   => $type,
                blog_id => $blog_id,
                id      => $obj->id,
                saved   => 1,
            }
        )
    );
}

sub _set_start_upload_params {
    my $app = shift;
    my ($param) = @_;

    if (my $perms = $app->permissions) {
        return $app->error( $app->translate("Permission denied.") )
            unless $perms->can_do('upload');

        my $blog_id = $app->param('blog_id');
        require MT::Blog;
        my $blog = MT::Blog->load($blog_id)
            or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));

        $param->{enable_archive_paths} = $blog->column('archive_path');
        $param->{local_site_path}      = $blog->site_path;
        $param->{local_archive_path}   = $blog->archive_path;
        my $label_path;
        if ( $param->{enable_archive_paths} ) {
            $label_path = $app->translate('Archive Root');
        }
        else {
            $label_path = $app->translate('Site Root');
        }
        my @extra_paths;
        my $date_stamp = epoch2ts( $blog, time );
        $date_stamp =~ s!^(\d\d\d\d)(\d\d)(\d\d).*!$1/$2/$3!;
        my $path_hash = {
            path  => $date_stamp,
            label => '<' . $label_path . '>' . '/' . $date_stamp,
        };

        if ( exists( $param->{middle_path} )
            && ( $date_stamp eq $param->{middle_path} ) )
        {
            $path_hash->{selected} = 1;
            delete $param->{archive_path};
        }
        push @extra_paths, $path_hash;
        $param->{extra_paths} = \@extra_paths;
        $param->{refocus}     = 1;
        $param->{missing_paths} =
          (      ( defined $blog->site_path || defined $blog->archive_path )
              && ( -d $blog->site_path || -d $blog->archive_path ) ) ? 0 : 1;

        if ( $param->{missing_paths} ) {
            if (
                $app->user->is_superuser
                || $app->run_callbacks(
                    'cms_view_permission_filter.blog',
                    $app, $blog_id, $blog
                )
              )
            {
                $param->{have_permissions} = 1;
            }
        }

        $param->{enable_destination} = 1;
        
        my $data = $app->_build_category_list(
            blog_id => $blog_id,
            markers => 1,
            type    => 'folder',
        );
        my $top_cat = -1;
        my $cat_tree = [{
            id    => -1,
            label => '/',
            basename => '/',
            path  => [],
        }];
        foreach (@$data) {
            next unless exists $_->{category_id};
            $_->{category_path_ids} ||= [];
            unshift @{ $_->{category_path_ids} }, -1;
            push @$cat_tree,
              {
                id => $_->{category_id},
                label => $_->{category_label} . '/',
                basename => $_->{category_basename} . '/',
                path => $_->{category_path_ids} || [],
              };
        }
        $param->{category_tree} = $cat_tree;
    }
    else {
        $param->{local_site_path}      = '';
        $param->{local_archive_path}   = '';
    }
    my $require_type = $param->{require_type};
    $require_type =~ s/\W//g;
    $param->{require_type} = $require_type;

    $param;
}

sub _upload_file {
    my $app = shift;
    my (%upload_param) = @_;
    require MT::Image;

    if (my $perms = $app->permissions) {
        return $app->error( $app->translate("Permission denied.") )
          unless $app->can_do('upload');
    }

    $app->validate_magic() or return;

    my $q = $app->param;
    my ($fh, $info) = $app->upload_info('file');
    my $mimetype;
    if ($info) {
        $mimetype = $info->{'Content-Type'};
    }
    my $has_overwrite = $q->param('overwrite_yes') || $q->param('overwrite_no');
    my %param = (
        entry_insert => $q->param('entry_insert'),
        middle_path  => $q->param('middle_path'),
        edit_field   => $q->param('edit_field'),
        site_path    => $q->param('site_path'),
        extra_path   => $q->param('extra_path'),
        upload_mode  => $app->mode,
    );
    return start_upload( $app, %param,
        error => $app->translate("Please select a file to upload.") )
      if !$fh && !$has_overwrite;
    my $basename = $q->param('file') || $q->param('fname');
    $basename =~ s!\\!/!g;    ## Change backslashes to forward slashes
    $basename =~ s!^.*/!!;    ## Get rid of full directory paths
    if ( $basename =~ m!\.\.|\0|\|! ) {
        return start_upload( $app, %param,
            error => $app->translate( "Invalid filename '[_1]'", $basename ) );
    }
    $basename = Encode::is_utf8( $basename ) ? $basename
              :                                Encode::decode( $app->charset, File::Basename::basename($basename) )
              ;

    if (my $asset_type = $upload_param{require_type}) {
        require MT::Asset;
        my $asset_pkg = MT::Asset->handler_for_file($basename);

        my %settings_for = (
            audio => {
                class => 'MT::Asset::Audio',
                error => $app->translate( "Please select an audio file to upload." ),
            },
            image => {
                class => 'MT::Asset::Image',
                error => $app->translate( "Please select an image to upload." ),
            },
            video => {
                class => 'MT::Asset::Video',
                error => $app->translate( "Please select a video to upload." ),
            },
        );

        if (my $settings = $settings_for{$asset_type}) {
            return start_upload( $app, %param, error => $settings->{error} )
                if !$asset_pkg->isa($settings->{class});
        }
    }

    my ($blog_id, $blog, $fmgr, $local_file, $asset_file, $base_url,
      $asset_base_url, $relative_url, $relative_path);
    if ($blog_id = $q->param('blog_id')) {
        $param{blog_id} = $blog_id;
        require MT::Blog;
        $blog = MT::Blog->load($blog_id)
            or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
        $fmgr = $blog->file_mgr;

        ## Set up the full path to the local file; this path could start
        ## at either the Local Site Path or Local Archive Path, and could
        ## include an extra directory or two in the middle.
        my ( $root_path, $middle_path );
        if ( $q->param('site_path') ) {
            $root_path = $blog->site_path;
        }
        else {
            $root_path = $blog->archive_path;
        }
        return $app->error(
            $app->translate(
                'Movable Type was unable to write on the "Upload Destination". Please make sure that the folder is writable from the web server.'
            )
        ) unless -d $root_path;
        $relative_path = $q->param('extra_path');
        $middle_path = $q->param('middle_path') || '';
        my $relative_path_save = $relative_path;
        if ( $middle_path ne '' ) {
            $relative_path =
              $middle_path . ( $relative_path ? '/' . $relative_path : '' );
        }
        my $path = $root_path;
        if ($relative_path) {
            if ( $relative_path =~ m!\.\.|\0|\|! ) {
                return start_upload( $app,
                    %param,
                    error => $app->translate(
                        "Invalid extra path '[_1]'", $relative_path
                    )
                );
            }
            $path = File::Spec->catdir( $path, $relative_path );
            ## Untaint. We already checked for security holes in $relative_path.
            ($path) = $path =~ /(.+)/s;
            ## Build out the directory structure if it doesn't exist. DirUmask
            ## determines the permissions of the new directories.
            unless ( $fmgr->exists($path) ) {
                $fmgr->mkpath($path)
                  or return start_upload( $app,
                    %param,
                    error => $app->translate(
                        "Can't make path '[_1]': [_2]",
                        $path, $fmgr->errstr
                    )
                  );
            }
        }
        $relative_url =
          File::Spec->catfile( $relative_path, encode_url($basename) );
        $relative_path = $relative_path
          ? File::Spec->catfile( $relative_path, $basename )
          : $basename;
        $asset_file = $q->param('site_path') ? '%r' : '%a';
        $asset_file = File::Spec->catfile( $asset_file, $relative_path );
        $local_file = File::Spec->catfile( $path, $basename );
        $base_url = $app->param('site_path') ? $blog->site_url
          : $blog->archive_url;
        $asset_base_url = $app->param('site_path') ? '%r' : '%a';

        ## Untaint. We have already tested $basename and $relative_path for security
        ## issues above, and we have to assume that we can trust the user's
        ## Local Archive Path setting. So we should be safe.
        ($local_file) = $local_file =~ /(.+)/s;

        my $real_fh;
        unless ($has_overwrite) {
            my ($w_temp, $h_temp, $ext_temp, $write_file_temp) = MT::Image->check_upload(
                Fh => $fh, Fmgr => $fmgr, Local => $local_file,
                Max => $upload_param{max_size},
                MaxDim => $upload_param{max_image_dimension}
            );
            if ($w_temp && $h_temp && $ext_temp) {
                my $ext_old = ( File::Basename::fileparse( $local_file, qr/[A-Za-z0-9]+$/ ) )[2];
                if (lc($ext_temp) ne lc($ext_old) && ! ( lc($ext_old) eq 'jpeg' && lc($ext_temp) eq 'jpg' )  && ! ( lc($ext_old) eq 'swf' && lc($ext_temp) eq 'cws' )) {
                    $ext_temp = lc($ext_temp);
                    my $target_file = $local_file;
                    $target_file =~ s/$ext_old$/$ext_temp/;
                    $relative_path =~ s/$ext_old$/$ext_temp/;
                    $relative_url =~ s/$ext_old$/$ext_temp/;
                    $asset_file =~ s/$ext_old$/$ext_temp/;
                    $basename =~ s/$ext_old$/$ext_temp/;
                    rename($local_file, $target_file);
                    $local_file =~ s/$ext_old$/$ext_temp/;
                    $real_fh =~ s/$ext_old$/$ext_temp/;
                    $app->param("changed_file_ext", "$ext_old,$ext_temp");
                }
            }
        }

        ## If $local_file already exists, we try to write the upload to a
        ## tempfile, then ask for confirmation of the upload.
        if ( $fmgr->exists($local_file) ) {
            if ($has_overwrite) {
                my $tmp = $q->param('temp');
                if ( $tmp =~ m!([^/]+)$! ) {
                    $tmp = $1;
                }
                else {
                    return $app->error(
                        $app->translate( "Invalid temp file name '[_1]'", $tmp ) );
                }
                my $tmp_dir = $app->config('TempDir');
                my $tmp_file = File::Spec->catfile( $tmp_dir, $tmp );
                if ( $q->param('overwrite_yes') ) {
                    $fh = gensym();
                    open $fh, $tmp_file
                      or return $app->error(
                        $app->translate(
                            "Error opening '[_1]': [_2]",
                            $tmp_file, "$!"
                        )
                      );
                }
                else {
                    if ( -e $tmp_file ) {
                        unlink($tmp_file)
                          or return $app->error(
                            $app->translate(
                                "Error deleting '[_1]': [_2]",
                                $tmp_file, "$!"
                            )
                          );
                    }
                    return start_upload($app);
                }
            }
            else {
                eval { require File::Temp };
                if ($@) {
                    return $app->error(
                        $app->translate(
                            "File with name '[_1]' already exists. (Install "
                              . "File::Temp if you'd like to be able to overwrite "
                              . "existing uploaded files.)",
                            $basename
                        )
                    );
                }
                my $tmp_dir = $app->config('TempDir');
                my ( $tmp_fh, $tmp_file );
                eval {
                    ( $tmp_fh, $tmp_file ) =
                      File::Temp::tempfile( DIR => $tmp_dir );
                };
                if ($@) {    #!$tmp_fh
                    return $app->errtrans(
                        "Error creating temporary file; please check your TempDir "
                          . "setting in your coniguration file (currently '[_1]') "
                          . "this location should be writable.",
                        (
                              $tmp_dir
                            ? $tmp_dir
                            : '[' . $app->translate('unassigned') . ']'
                        )
                    );
                }
                defined( _write_upload( $fh, $tmp_fh ) )
                  or return $app->error(
                    $app->translate(
                        "File with name '[_1]' already exists; Tried to write "
                          . "to tempfile, but open failed: [_2]",
                        $basename,
                        "$!"
                    )
                  );
                close $tmp_fh;
                my ( $vol, $path, $tmp ) = File::Spec->splitpath($tmp_file);
                my ($ext_from, $ext_to) = split(",", $app->param('changed_file_ext')); 
                my $extension_message = $app->translate("Extension changed from [_1] to [_2]", $ext_from, $ext_to) if ($ext_from && $ext_to);
                my $dialog = $app->param('dialog') ? 1 : 0;
                return $app->load_tmpl(
                    $dialog ? 'dialog/asset_replace.tmpl' : 'asset_replace.tmpl',
                    {
                        temp         => $tmp,
                        extra_path   => $relative_path_save,
                        site_path    => scalar $q->param('site_path'),
                        asset_select => $q->param('asset_select'),
                        entry_insert => $q->param('entry_insert'),
                        edit_field   => $app->param('edit_field'),
                        middle_path  => $middle_path,
                        fname        => $basename,
                        no_insert    => $q->param('no_insert') || "",
                        extension_message => $extension_message,
                    }
                );
            }
        }
    }
    else {
        $blog_id        = 0;
        $asset_base_url = '%s/uploads';
        $base_url       = $app->support_directory_url . 'uploads';
        $param{support_path} =
          File::Spec->catdir( $app->support_directory_path, 'uploads' );

        require MT::FileMgr;
        $fmgr = MT::FileMgr->new('Local');
        unless ( $fmgr->exists( $param{support_path} ) ) {
            $fmgr->mkpath( $param{support_path} );
            unless ( $fmgr->exists( $param{support_path} ) ) {
                return $app->error( $app->translate(
                    "Could not create upload path '[_1]': [_2]",
                        $param{support_path}, $fmgr->errstr
                ) );
            }
        }

        require File::Basename;
        my ($stem, undef, $type) = File::Basename::fileparse( $basename,
            qr/\.[A-Za-z0-9]+$/ );
        my $unique_stem = $stem;
        $local_file = File::Spec->catfile( $param{support_path},
            $unique_stem . $type );
        my $i = 1;
        while ($fmgr->exists($local_file)) {
            $unique_stem = join q{-}, $stem, $i++;
            $local_file = File::Spec->catfile( $param{support_path},
                $unique_stem . $type );
        }

        my $unique_basename = $unique_stem . $type;
        $relative_path  = $unique_basename;
        $relative_url   = encode_url($unique_basename);
        $asset_file     = File::Spec->catfile( '%s', 'uploads',
          $unique_basename );
    }

    if ( my $allow_exts = $app->config('AssetFileExtensions') ) {
        my @allow_exts = map { if ( $_ =~ m/^\./ ) { qr/$_/i } else { qr/\.$_/i } } split '\s?,\s?', $allow_exts;
        my @ret = File::Basename::fileparse( $local_file, @allow_exts );
        unless ( $ret[2] ) {
            return $app->error($app->translate('The file([_1]) you uploaded is not allowed.', $basename));
        }
    }

    my ($w, $h, $id, $write_file) = MT::Image->check_upload(
        Fh => $fh, Fmgr => $fmgr, Local => $local_file,
        Max => $upload_param{max_size},
        MaxDim => $upload_param{max_image_dimension}
    );

    return $app->error(MT::Image->errstr)
        unless $write_file;

    ## File does not exist, or else we have confirmed that we can overwrite.
    my $umask = oct $app->config('UploadUmask');
    my $old   = umask($umask);
    defined( my $bytes = $write_file->() )
      or return $app->error(
        $app->translate(
            "Error writing upload to '[_1]': [_2]", $local_file,
            $fmgr->errstr
        )
      );
    umask($old);

    ## Close up the filehandle.
    close $fh;

    ## If we are overwriting the file, that means we still have a temp file
    ## lying around. Delete it.
    if ( $q->param('overwrite_yes') ) {
        my $tmp = $q->param('temp');
        if ( $tmp =~ m!([^/]+)$! ) {
            $tmp = $1;
        }
        else {
            return $app->error(
                $app->translate( "Invalid temp file name '[_1]'", $tmp ) );
        }
        my $tmp_file = File::Spec->catfile( $app->config('TempDir'), $tmp );
        unlink($tmp_file)
          or return $app->error(
            $app->translate( "Error deleting '[_1]': [_2]", $tmp_file, "$!" ) );
    }

    ## We are going to use $relative_path as the filename and as the url passed
    ## in to the templates. So, we want to replace all of the '\' characters
    ## with '/' characters so that it won't look like backslashed characters.
    ## Also, get rid of a slash at the front, if present.
    $relative_path =~ s!\\!/!g;
    $relative_path =~ s!^/!!;
    $relative_url  =~ s!\\!/!g;
    $relative_url  =~ s!^/!!;
    my $url = $base_url;
    $url .= '/' unless $url =~ m!/$!;
    $url .= $relative_url;
    my $asset_url = $asset_base_url . '/' . $relative_url;

    require File::Basename;
    my $local_basename = File::Basename::basename($local_file);
    my $ext =
      ( File::Basename::fileparse( $local_file, qr/[A-Za-z0-9]+$/ ) )[2];

    require MT::Asset;
    my $asset_pkg = MT::Asset->handler_for_file($local_basename);
    my $is_image = 0;
    if ( defined($w) && defined($h) ) {
        $is_image = 1
            if $asset_pkg->isa('MT::Asset::Image');
    }
    else {
        # rebless to file type
        $asset_pkg = 'MT::Asset'
            if $asset_pkg->isa('MT::Asset::Image');
    }
    return $app->errtrans('Uploaded file is not an image.')
        if !$is_image
        && exists( $upload_param{require_type} )
        && $upload_param{require_type} eq 'image';
    my $asset;
    if (
        !(
            $asset = $asset_pkg->load(
                { file_path => $asset_file, blog_id => $blog_id },
                { binary => { file_path => 1 } }
            )
        )
      )
    {
        $asset = $asset_pkg->new();
        $asset->file_path($asset_file);
        $asset->file_name($local_basename);
        $asset->file_ext($ext);
        $asset->blog_id($blog_id);
        $asset->label($local_basename);
        $asset->created_by( $app->user->id );
    }
    else {
        $asset->modified_by( $app->user->id );
    }
    my $original = $asset->clone;
    $asset->url($asset_url);

    if ($is_image) {
        $asset->image_width($w);
        $asset->image_height($h);
    }

    $asset->mime_type($mimetype) if $mimetype;

    $asset->save;
    $app->run_callbacks( 'cms_post_save.asset', $app, $asset, $original );

    if ($is_image) {
        $app->run_callbacks(
            'cms_upload_file.' . $asset->class,
            File  => $local_file,
            file  => $local_file,
            Url   => $url,
            url   => $url,
            Size  => $bytes,
            size  => $bytes,
            Asset => $asset,
            asset => $asset,
            Type  => 'image',
            type  => 'image',
            Blog  => $blog,
            blog  => $blog
        );
        $app->run_callbacks(
            'cms_upload_image',
            File       => $local_file,
            file       => $local_file,
            Url        => $url,
            url        => $url,
            Size       => $bytes,
            size       => $bytes,
            Asset      => $asset,
            asset      => $asset,
            Height     => $h,
            height     => $h,
            Width      => $w,
            width      => $w,
            Type       => 'image',
            type       => 'image',
            ImageType  => $id,
            image_type => $id,
            Blog       => $blog,
            blog       => $blog
        );
    }
    else {
        $app->run_callbacks(
            'cms_upload_file.' . $asset->class,
            File  => $local_file,
            file  => $local_file,
            Url   => $url,
            url   => $url,
            Size  => $bytes,
            size  => $bytes,
            Asset => $asset,
            asset => $asset,
            Type  => 'file',
            type  => 'file',
            Blog  => $blog,
            blog  => $blog
        );
    }

    return ($asset, $bytes);
}

sub _write_upload {
    my ( $upload_fh, $dest_fh ) = @_;
    my $fh = gensym();
    if ( ref($dest_fh) eq 'GLOB' ) {
        $fh = $dest_fh;
    }
    else {
        open $fh, ">$dest_fh" or return;
    }
    binmode $fh;
    binmode $upload_fh;
    my ( $bytes, $data ) = (0);
    while ( my $len = read $upload_fh, $data, 8192 ) {
        print $fh $data;
        $bytes += $len;
    }
    close $fh;
    $bytes;
}

1;
